﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Mono.Cecil;
using Mono.Cecil.Cil;
using System.IO;

namespace cs746
{
    class CecilAnalyzer
    {
        private static HashSet<string> _allObjects;
        private static HashSet<string> _repairedObjects;

        public static TAData Analyze(string assemblyName, List<String> libs, string sourcecodeDirectory, bool createFileTypeLinks)
        {
            _allObjects = new HashSet<string>();
            _repairedObjects = new HashSet<string>();
            var ta = new TAData();
            FileInfo f = new FileInfo(assemblyName);

            AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(f.FullName);

            foreach (string l in libs)
            {
                FileInfo f1 = new FileInfo(l);
                assembly.Modules.Add(ModuleDefinition.ReadModule(f1.FullName));
            }

            //Can have multiple modules in an assembly
            foreach (var module in assembly.Modules)
            {
                Console.BufferWidth = 200;
                //Console.WindowWidth = 200;
                Console.BufferHeight = 1000;
                Console.WriteLine(module);
                foreach (var type in module.Types)
                {
                    if (type.Name == "<Module>") continue; // <Module> hidden type - exclude it
                    if (type.IsInterface) ta.AddObject(type.FullName, ObjectType.Interface);
                    
                    else if (type.IsClass && type.IsEnum) continue;
                    else if (type.IsClass && type.BaseType.Name == "MulticastDelegate") ta.AddObject(type.FullName, ObjectType.Delegate);
                    else if (type.IsClass && type.BaseType.FullName == "System.ValueType") ta.AddObject(type.FullName, ObjectType.Struct);
                    else if (type.IsClass && !type.IsEnum && type.BaseType.Name != "MulticastDelegate") ta.AddObject(type.FullName, ObjectType.Class);
                    else throw new Exception();

                    WriteLineToConsole(type.FullName, 0);
                    Console.WriteLine("\t{0}", type.FullName);
                    foreach (var method in type.Methods)
                    {
                        //if (method.IsConstructor) ta.AddObject(method.FullName,ObjectType.Ctor);
                        //else if (method.IsVirtual) ta.AddObject(method.FullName, ObjectType.VirtualFunction);
                        //else ta.AddObject(method.FullName, ObjectType.Function); // it's not correct 
                        //WriteLineToConsole(method.ToString(), 1);

                        ta.AddEdge(type.FullName, method.FullName, EdgeType.Contains);

                        //ParseMethodBody(ta, method, 2, new List<string> { method.ToString() }); 

                        //Console.WriteLine();
                        ParseMethodBodyNew(ta, method, 1, new List<string>());
                        //Console.WriteLine();

                    }
                    //Console.WriteLine();
                }
            }
            Console.WriteLine("\ntypes/edges done\n");
            if (createFileTypeLinks) ta.AddTypeFileLinks(sourcecodeDirectory);
            ta.MakeItBetter();
            ta.MapUnknownNamespaces();

            //var tt = ta.NodeWithoutSourceCode();

            //Console.ReadKey();
            return ta;

        }

        static void ParseMethodBodyNew(TAData ta, object ins, int step, List<string> callstack)
        {
            if (_allObjects.Contains(((MethodReference)ins).FullName))
            {
                if ((ins is MethodDefinition) && !_repairedObjects.Contains(((MethodReference)ins).FullName)) _repairedObjects.Add(((MethodReference)ins).FullName);
                else
                {
                    return;
                }
            }
            else 
            { 
                _allObjects.Add(((MethodReference)ins).FullName);
                if (ins is MethodDefinition) _repairedObjects.Contains(((MethodReference)ins).FullName);
            }
            var method = ins as MethodDefinition;
            if (method != null && method.HasBody)
            {
                WriteLineToConsole(method.ToString(), step);
                if (method.IsVirtual) ta.AddObject(method.FullName, ObjectType.VirtualFunction);
                else if (method.IsConstructor) ta.AddObject(method.FullName, ObjectType.Ctor);
                else if (method.IsStatic) ta.AddObject(method.FullName, ObjectType.StaticFunction);
                else ta.AddObject(method.FullName, ObjectType.Function);

                foreach (var item in method.Body.Instructions)
                {
                    if (!(item.OpCode.Name == "call" || item.OpCode.Name == "callvirt" || item.OpCode.Name == "newobj")) 
                    { 
                        continue; 
                    }
                    if (callstack.Contains(item.Operand.ToString()))
                    {
                        WriteLineToConsole("<< Recursion >>", step + 1);
                        ta.AddEdge(method.FullName, ((MethodDefinition)item.Operand).FullName, EdgeType.RecursiveCall); 
                    }
                    else if (item.OpCode.Name == "call" || item.OpCode.Name == "callvirt")
                    {                        
                        //WriteLineToConsole(item.Operand.ToString(), step +1 );

                        ta.AddEdge(method.FullName, ((MethodReference)item.Operand).FullName, item.OpCode.Name == "call" ? EdgeType.Call : EdgeType.VirtualCall);

                        callstack.Add(item.Operand.ToString());
                        ParseMethodBodyNew(ta, item.Operand, step + 1, callstack);
                        callstack.Remove(item.Operand.ToString());                        
                    }
                    else if (item.OpCode.Name == "newobj")
                    {
                        //WriteLineToConsole(item.Operand.ToString(), step +1);

                        ta.AddEdge(method.FullName, ((MethodReference)item.Operand).FullName,  EdgeType.CtorCall);

                        callstack.Add(item.Operand.ToString());
                        ParseMethodBodyNew(ta, item.Operand, step + 1, callstack);
                        callstack.Remove(item.Operand.ToString());
                    }
                    //else WriteLineToConsole(item.OpCode.Name, step); 
                }
            }
            else
            {
                var methodRef = ins as MethodReference;
                if (methodRef != null)
                {
                    WriteLineToConsole(methodRef.ToString(), step);
                    ta.AddObject(methodRef.FullName, ObjectType.MethodReference);
                    //WriteLineToConsole(methodRef.Module.FullyQualifiedName.ToString(), step);                    
                }
                else throw new Exception();
            }
        }

        static void WriteLineToConsole(string value, int numTabs)
        {
            return;
            for (int i = 0; i < numTabs; i++)
            {
                Console.Write("\t");
            }
            Console.WriteLine(value);
        }
    }
}
